' 
' Copyright (C) 2014 Infinity CCS Ltd (http://www.infinityccs.com/)
' Copyright (C) 2007 Sasa Coh <sasacoh@gmail.com>
'
' This program is free software; you can redistribute it and/or modify
' it under the terms of the GNU General Public License as published by
' the Free Software Foundation; either version 3 of the License, or
' (at your option) any later version.
'
' This program is distributed in the hope that it will be useful,
' but WITHOUT ANY WARRANTY; without even the implied warranty of
' MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
' GNU General Public License for more details.
'
' You should have received a copy of the GNU General Public License
' along with this program.  If not, see <http://www.gnu.org/licenses/>. 
'/

Imports System.Collections
Imports System.Collections.Generic
Imports System
Imports Infinity.pjsipNET.Common
Imports Infinity.pjsipNET.Common.CallState
Imports Infinity.pjsipNET.Common.Media
Imports Infinity.pjsipNET.Common.Config

Namespace CallControl

  Public Delegate Sub DCallStateRefresh(sessionId As Integer)
  Public Delegate Sub DIncomingCallNotification(sessionId As Integer, cli As String, ddi As String, clidisplay As String)

  '//////////////////////////////////////////////////////////////////////////
  ''' <summary>
  ''' CCallManager
  ''' Main telephony class. Manages call instances. Handles user events and dispatches to a proper 
  ''' call instance automaton. 
  ''' </summary>
  Public Class CallManager
#Region "Variables"

    Private Shared _instance As CallManager = Nothing

    Private _calls As Dictionary(Of Integer, IStateMachine)

    Private _factory As IFactory = New IFactory.NullFactory()

    Dim _pendingHoldConfirmAction As HoldConfirmAction

#End Region

#Region "Properties"

    Public Property Factory As IFactory
      Get
        Return _factory
      End Get
      Set(value As IFactory)
        _factory = value
      End Set
    End Property

    Private _media As IMediaProxy = New NullMediaProxy
    Public Property MediaProxy As IMediaProxy
      Get
        Return _media
      End Get
      Set(value As IMediaProxy)
        _media = value
      End Set
    End Property

    Private _callLog As CallLog.ICallLog = New CallLog.NullCallLogger()
    Public Property CallLogger As CallLog.ICallLog
      Get
        Return _callLog
      End Get
      Set(value As CallLog.ICallLog)
        _callLog = value
      End Set
    End Property

    Private _stack As IVoipProxy = New IVoipProxy.NullVoipProxy()
    Public Property StackProxy As IVoipProxy
      Get
        Return _stack
      End Get
      Set(value As IVoipProxy)
        _stack = value
      End Set
    End Property

    Private _config As IConfigurator = New NullConfigurator()
    Public Property Config As IConfigurator
      Get
        Return _config
      End Get
      Set(value As IConfigurator)
        _config = value
      End Set
    End Property

    ''' <summary>
    ''' Call indexer. 
    ''' Retrieve a call instance (IStateMachine) from a call list. 
    ''' </summary>
    ''' <param name="sessionId">call/session identification</param>
    ''' <returns>an instance of a call with a given sessionId</returns>
    Default Public ReadOnly Property item(sessionId As Integer) As IStateMachine
      Get
        If ((_calls.Count = 0) OrElse (Not _calls.ContainsKey(sessionId))) Then
          Return New NullStateMachine()
        End If
        Return _calls(sessionId)
      End Get
    End Property

    ''' <summary>
    ''' Retrieve a complete list of calls (IStateMachines)
    ''' </summary>
    Public ReadOnly Property CallList As Dictionary(Of Integer, IStateMachine)
      Get
        Return _calls
      End Get
    End Property

    Public ReadOnly Property Count As Integer
      Get
        Return _calls.Count
      End Get
    End Property

    Public ReadOnly Property Is3Pty As Boolean
      Get
        Return If(Me(EStateId.ACTIVE).Count = 2, True, False)
      End Get
    End Property

    Private _initialized As Boolean = False
    Public ReadOnly Property IsInitialized As Boolean
      Get
        Return _initialized
      End Get
    End Property

    Default Public ReadOnly Property Item(stateId As EStateId) As List(Of IStateMachine)
      Get
        Dim calls As New List(Of IStateMachine)
        For Each sm As KeyValuePair(Of Integer, IStateMachine) In _calls
          If (sm.Value.State.Id And stateId) > 0 Then
            calls.Add(sm.Value)
          End If
        Next
        Return calls
      End Get
    End Property

#End Region 'Properties

#Region "Constructor"

    ''' <summary>
    ''' CCallManager Singleton
    ''' </summary>
    ''' <returns></returns>
    Public Shared ReadOnly Property Instance As CallManager
      Get
        If (_instance Is Nothing) Then _instance = New CallManager()
        Return _instance
      End Get
    End Property
#End Region 'Constructor

#Region "Events"

    ''' <summary>
    ''' Notify about call state changed in automaton with given sessionId
    ''' </summary>
    Public Event CallStateRefresh As DCallStateRefresh

    Public Event IncomingCallNotification As DIncomingCallNotification






    '/////////////////////////////////////////////////////////////////////////
    '// Callback handlers
    ''' <summary>
    ''' Inform GUI to be refreshed 
    ''' </summary>
    Public Sub updateGui(sessionId As Integer)
      '// check if call is in table (doesn't work in connecting state - session not inserted in call table)
      '//if (!_calls.ContainsKey(sessionId)) return;

      RaiseEvent CallStateRefresh(sessionId)
    End Sub

#End Region 'Events

#Region "Public methods"

    ''' <summary>
    ''' Initialize telephony and VoIP stack. On success register accounts.
    ''' </summary>
    ''' <returns>initialiation status</returns>
    Public Function Initialize(proxy As IVoipProxy) As Integer
      _stack = proxy

      Dim status As Integer = 0
      If (Not IsInitialized) Then
        '//// register to signaling proxy interface
        AddHandler ICallProxy.CallStateChanged, AddressOf OnCallStateChanged
        AddHandler ICallProxy.CallIncoming, AddressOf OnIncomingCall
        AddHandler ICallProxy.CallNotification, AddressOf OnCallNotification

        '// Initialize call table
        _calls = New Dictionary(Of Integer, IStateMachine)
      End If

      '// (re)initialize voip proxy
      status = StackProxy.initialize()
      If (status <> 0) Then Return status

      '// (re)register 
      _initialized = True
      Return status
    End Function

    ''' <summary>
    ''' Shutdown telephony and VoIP stack
    ''' </summary>
    Public Sub Shutdown()
      If CallList IsNot Nothing Then
        Dim callarr(CallList.Count - 1) As IStateMachine
        CallList.Values.CopyTo(callarr, 0)
        For i As Integer = 0 To callarr.Length - 1
          callarr(i).Destroy()
        Next

        Me.CallList.Clear()
      End If
      StackProxy.shutdown()
      _initialized = False

      'CallStateRefresh = Nothing
      'IncomingCallNotification = Nothing

      RemoveHandler ICallProxy.CallStateChanged, AddressOf OnCallStateChanged
      RemoveHandler ICallProxy.CallIncoming, AddressOf OnIncomingCall
      RemoveHandler ICallProxy.CallNotification, AddressOf OnCallNotification
      RemoveHandler StackProxy.CallReplaced, AddressOf OnCallReplaced
    End Sub

    ' ''' <summary>
    ' ''' Create outgoing call using default accountId. 
    ' ''' </summary>
    ' ''' <param name="number">Number to call</param>
    'Public Function CreateSimpleOutboundCall(number As String) As Integer
    '  Dim accId As Integer = Config.DefaultAccountIndex
    '  Return Me.CreateSimpleOutboundCall(number, accId)
    'End Function

    ''' <summary>
    ''' Try to create an outbound call. No automatics: make call only if no other call exists
    ''' </summary>
    ''' <param name="number"></param>
    ''' <returns>SessionId or -1 on error</returns>
    Public Function CreateSimpleOutboundCall(number As String, accountKey As String) As Integer
      '// if no calls in list
      If ((Me.Count > 0) OrElse (Not IsInitialized)) Then
        Return -1
      Else
        '// create state machine
        Dim sm As IStateMachine = Factory.createStateMachine()
        '// couldn't create new call instance (max calls?)
        If (sm Is Nothing) Then
          Return -1
        End If

        '// make call request (stack provides new sessionId)
        Dim newsession As Integer = sm.State.makeCall(number, accountKey)
        If (newsession = -1) Then
          Return -1
        End If

        '// update call table
        '// catch argument exception (same key)!!!!
        Try
          sm.SessionId = newsession
          sm.AccountId = Config.Account(accountKey).pjsipIndex
          _calls.Add(newsession, sm)
        Catch e As ArgumentException
          '// previous call not released ()
          '// first release old one
          _calls(newsession).Destroy()
          '// and then add new one
          _calls.Add(newsession, sm)
        End Try

        Return sm.SessionId
      End If
    End Function

    ''' <summary>
    ''' Create outgoing call to a number and from a given account.
    ''' 
    ''' If the other calls exist check if it is possible to create a new one.
    ''' The logic below will automatically put the active call on hold, 
    ''' return -2 and store a new call creation request. When hold confirmation 
    ''' received create a call. 
    ''' 
    ''' Be aware: This is done asynchronously. 
    ''' 
    ''' </summary>
    ''' <param name="number">Number to call</param>
    ''' <param name="line">Line number (1 or 2) from interface</param>
    Public Function CreateSmartOutboundCall(number As String, line As Integer, accountKey As String) As Integer
      If (Not IsInitialized) Then Return -1

      '// check if current call automatons allow session creation.
      If (Me.GetNoCallsInStates(CInt(EStateId.CONNECTING Or EStateId.ALERTING Or EStateId.ACTIVE)) > 0) Then
        '// new call not allowed!
        Return -1
      End If
      '// if at least 1 call connected try to put it on hold first
      'If (Me(EStateId.ACTIVE).Count = 0) Then
      '// create state machine
      Dim sm As IStateMachine = Factory.createStateMachine()
      '// couldn't create new call instance (max calls?)
      If (sm Is Nothing) Then
        Return -1
      End If

      '// make call request (stack provides new sessionId)
      Dim newsession As Integer = sm.State.makeCall(number, accountKey)
      If (newsession = -1) Then
        Return -1
      End If

      '// update call table
      '// catch argument exception (same key)!!!!
      Try
        sm.SessionId = newsession
        sm.Line = line
        sm.AccountId = Config.Account(accountKey).pjsipIndex
        _calls.Add(newsession, sm)

      Catch e As ArgumentException
        '// previous call not released ()
        '// first release old one
        _calls(newsession).Destroy()
        '// and then add new one
        _calls.Add(newsession, sm)
      End Try

      Return sm.SessionId
      'Else '// we have at least one ACTIVE call
      '  '// put connected call on hold
      '  _pendingHoldConfirmAction = New PendingAction.CreateSession(number, line, accountKey)

      '  Dim calls As List(Of IStateMachine) = Me(EStateId.ACTIVE)
      '  If (calls.Count > 0) Then
      '    calls(0).State.holdCall()
      '  End If
      '  '// indicates that new call is pending...
      '  '// At this point we don't know yet if the call will be created or not
      '  '// The call will be created when current call is put on hold (confirmation)!
      '  Return -2
      'End If

    End Function


    ''' <summary>
    ''' User triggers a call release for a given session
    ''' </summary>
    ''' <param name="session">session identification</param>
    Public Sub OnUserRelease(session As Integer)
      Me(session).State.endCall()
    End Sub

    ''' <summary>
    ''' User accepts call for a given session
    ''' In case of multi call put current active call to Hold
    ''' </summary>
    ''' <param name="session">session identification</param>
    Public Sub OnUserAnswer(session As Integer)
      Dim list As List(Of IStateMachine) = Me(EStateId.ACTIVE)
      '// should not be more than 1 call active
      If (list.Count > 0) Then
        '// put it on hold
        Dim sm As IStateMachine = list(0)
        If Not sm.IsNull Then sm.State.holdCall()

        '// set ANSWER event pending for HoldConfirm
        _pendingHoldConfirmAction = New HoldConfirmAction.UserAnswer(session)
      Else
        Me(session).State.acceptCall()
      End If
    End Sub

    ''' <summary>
    ''' User put call on hold or retrieve 
    ''' </summary>
    ''' <param name="session">session identification</param>
    Public Sub OnUserHold(session As Integer)
      '// check Hold
      Dim state As IState = Me(session).State
      If (state.Id = EStateId.ACTIVE) Then
        Me(session).State.holdCall()
      Else
        '// illegal
      End If
    End Sub

    ''' <summary>
    ''' Retrieve a call on hold 
    ''' </summary>
    ''' <param name="session">session identification</param>
    Public Sub OnUserRetrieve(session As Integer)
      '// check Hold or Retrieve
      Dim state As IState = Me(session).State
      If (state.Id = EStateId.HOLDING) Then
        '// execute retrieve
        '// check if any ACTIVE calls
        If (Me(EStateId.ACTIVE).Count > 0) Then

          '// get 1st and put it on hold
          Dim sm As IStateMachine = Me(EStateId.ACTIVE)(0)
          If Not sm.IsNull Then sm.State.holdCall()

          '// set Retrieve event pending for HoldConfirm
          _pendingHoldConfirmAction = New HoldConfirmAction.UserRetrieve(session)
        Else
          Me(session).State.retrieveCall()
        End If
      Else
        '// illegal
      End If
    End Sub


    ''' <summary>
    ''' User starts a call transfer
    ''' </summary>
    ''' <param name="session">session identification</param>
    ''' <param name="number">number to transfer</param>
    Public Sub OnUserTransfer(session As Integer, number As String)
      Me(session).State.xferCall(number)
    End Sub

    ''' <summary>
    ''' Attendant transfer
    ''' </summary>
    ''' <param name="session"></param>
    ''' <param name="partnerSession"></param>
    Public Sub OnUserTransferAttendant(session As Integer, partnerSession As Integer)
      Me(session).State.xferCallSession(partnerSession)
    End Sub


    ''' <summary>
    ''' 
    ''' </summary>
    ''' <param name="session"></param>
    ''' <param name="digits"></param>
    ''' <param name="mode"></param>
    Public Sub OnUserDialDigit(session As Integer, digits As String, mode As EDtmfMode)
      Me(session).State.dialDtmf(digits, mode)
    End Sub

    ''' <summary>
    ''' 
    ''' </summary>
    ''' <param name="session"></param>
    Public Sub OnUserConference(session As Integer)
      '// check preconditions: 1 call active, other held
      '// 1st if current call is held -> search if any active -> execute retrieve
      If ((Me(EStateId.ACTIVE).Count = 1) AndAlso (Me(EStateId.HOLDING).Count >= 1)) Then

        Dim calls As List(Of IStateMachine) = Me(EStateId.HOLDING)
        If (calls.Count > 0) Then
          calls(0).State.retrieveCall()
          '// set conference flag
          calls(0).State.conferenceCall()
        End If
        Exit Sub
      End If
    End Sub

    '''' <summary>
    '''' Send message inside call dialog
    '''' </summary>
    '''' <param name="sessionId"></param>
    '''' <returns></returns>
    'Public Function OnUserSendCallMessage(sessionId As Integer, message As String) As Boolean
    '  Return Me(sessionId).State.sendCallMessage(message)
    'End Function

#End Region  '// public methods

#Region "Internal & Private Methods"

    ''' <summary>
    ''' Destroy call 
    ''' </summary>
    ''' <param name="session">session identification</param>
    Friend Sub DestroySession(session As Integer)
      Dim notify As Boolean = True
      If (Me(session).DisableStateNotifications) Then
        notify = False
      End If
      _calls.Remove(session)
      '// Warning: this call no longer exists
      If (notify) Then updateGui(session)
    End Sub

    ''' <summary>
    ''' 
    ''' </summary>
    Friend Sub activatePendingAction()
      If (Nothing IsNot _pendingHoldConfirmAction) Then _pendingHoldConfirmAction.Activate()
      _pendingHoldConfirmAction = Nothing
    End Sub

    ''' <summary>
    ''' 
    ''' </summary>
    ''' <param name="callId"></param>
    ''' <param name="callState"></param>
    Private Sub OnCallStateChanged(callId As Integer, callState As ESessionState, info As String, lastStatus As Integer, lastStatusText As String)
      Dim sm As IStateMachine = Me(callId)
      If (sm.IsNull) Then Exit Sub
      sm.LastStatus = lastStatus
      sm.LastStatusText = lastStatusText

      Select Case (callState)
        Case ESessionState.SESSION_STATE_CALLING
          '//sm.getState().onCalling();
        Case ESessionState.SESSION_STATE_EARLY
          sm.State.onAlerting()
        Case ESessionState.SESSION_STATE_CONNECTING
          sm.State.onConnect()
        Case ESessionState.SESSION_STATE_DISCONNECTED
          sm.State.onReleased()
      End Select
    End Sub

    ''' <summary>
    ''' Create session for incoming call.
    ''' </summary>
    ''' <param name="sessionId">session identification</param>
    ''' <param name="cli">number from calling party</param>
    ''' <param name="clidisplay">display name of calling party</param>
    ''' <param name="ddi">DDI of the call</param>
    Private Sub OnIncomingCall(accountId As Integer, sessionId As Integer, cli As String, ddi As String, clidisplay As String)
      Dim incomingCall As IStateMachine = Factory.createStateMachine()
      '// save session parameters
      incomingCall.SessionId = sessionId
      incomingCall.AccountId = accountId

      '// check if callID already exists!!!
      If (CallList.ContainsKey(sessionId)) Then
        '// shouldn't be here
        '// release the call
        CallList(sessionId).State.endCall()
        Exit Sub
      End If
      '// add call to call table
      _calls.Add(sessionId, incomingCall)

      Dim sm As IStateMachine = Me(sessionId)

      If (sm.IsNull) Then Exit Sub

      '// inform automaton for incoming call
      sm.State.incomingCall(cli, ddi, clidisplay)

      '// call callback 
      If sm.DisableStateNotifications = False Then RaiseEvent IncomingCallNotification(sessionId, cli, ddi, clidisplay)
    End Sub

    Private Sub OnCallNotification(callId As Integer, notFlag As ECallNotification, text As String)
      If (notFlag = ECallNotification.CN_HOLDCONFIRM) Then
        Dim sm As IStateMachine = Me(callId)
        If Not sm.IsNull Then sm.State.onHoldConfirm()
      End If
    End Sub

    ''' <summary>
    ''' Replace call ids
    ''' </summary>
    ''' <param name="newid"></param>
    ''' <param name="oldid"></param>
    Private Sub OnCallReplaced(oldid As Integer, newid As Integer)
      Dim sm As IStateMachine = CallList(oldid)
      _calls.Remove(oldid)
      sm.SessionId = newid
      CallList.Add(newid, sm)
    End Sub

    ''' <summary>
    ''' Gets the number of calls in given states (bitmask)
    ''' 
    ''' example: 
    '''   getNoCallsInStates(EStateId.CONNECTING | EStateId.ALERTING)
    ''' 
    ''' </summary>
    ''' <param name="states">a bit mask of states</param>
    ''' <returns></returns>
    Private Function GetNoCallsInStates(states As Integer) As Integer
      Dim cnt As Integer = 0
      For Each kvp As KeyValuePair(Of Integer, IStateMachine) In _calls
        If ((states And CInt(kvp.Value.State.Id)) = CInt(kvp.Value.State.Id)) Then
          cnt += 1
        End If
      Next
      Return cnt
    End Function

#End Region 'Methods
    ''' <summary>
    ''' Internal mechanism to execute 2 stage actions. Some user events requires 
    ''' two request to VoIP side. Depending on result the second action is executed.
    ''' </summary>
    Class HoldConfirmAction
      ''' <summary>
      ''' Action definitions for pending events.
      ''' </summary>
      Enum EType As Integer
        EUserAnswer
        'ECreateSession
        EUserRetrieve
        'EConference
      End Enum

      'Delegate Sub DPendingAnswer(sessionId As Integer) '// for onUserAnswer
      'Delegate Sub DPendingCreateSession(number As String) '// for CreateOutboudCall

      Private _actionType As EType
      Private _sessionId As Integer
      'Private _number As String
      'Private _accountKey As String
      'Private _line As Integer = 0

      Private Sub New(action As EType, sessionId As Integer)
        _actionType = action
        _sessionId = sessionId
      End Sub

      'Private Sub New(action As EType, number As String, line As Integer, accountKey As String)
      '  _actionType = action
      '  _sessionId = -1
      '  _line = line
      '  _number = number
      '  _accountKey = accountKey
      'End Sub

      Public Sub Activate()
        Select Case _actionType
          Case EType.EUserAnswer
            CallManager.Instance.OnUserAnswer(_sessionId)
            'Case EType.ECreateSession
            'CCallManager.Instance.CreateSmartOutboundCall(_number, _line, _accountKey)
          Case EType.EUserRetrieve
            CallManager.Instance.OnUserRetrieve(_sessionId)
            'Case EType.EConference
            '  CCallManager.Instance.OnUserConference(_sessionId)
        End Select
      End Sub

      Class UserAnswer
        Inherits HoldConfirmAction
        Public Sub New(sessionId As Integer)
          MyBase.New(EType.EUserAnswer, sessionId)
        End Sub
      End Class
      'Class CreateSession
      '  Inherits PendingAction
      '  Public Sub New(number As String, line As Integer, accountKey As String)
      '    MyBase.New(EType.ECreateSession, number, line, accountKey)
      '  End Sub
      'End Class

      Class UserRetrieve
        Inherits HoldConfirmAction
        Public Sub New(sessionId As Integer)
          MyBase.New(EType.EUserRetrieve, sessionId)
        End Sub
      End Class
      'Class Conference
      '  Inherits PendingAction
      '  Public Sub New(sessionId As Integer)
      '    MyBase.New(EType.EConference, sessionId)
      '  End Sub
      'End Class

    End Class
  End Class

End Namespace
